/*
 * MIT License
 *
 * Copyright (c) 2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

export { };

const setExitCodeFunc: (code: number) => void | undefined = exchangeInnerMethod(process, "_setExitCode");
if (setExitCodeFunc !== undefined) {
    let g_processExitCode: any;
    Reflect.defineProperty(process, "exitCode", {
        get() {
            return g_processExitCode;
        },
        set(v: number) {
            g_processExitCode = v;
            setExitCodeFunc(v);
        },
    });
}

const innerKill: (pid: number, signal: number) => number = exchangeInnerMethod(process, "_innerKill");
const signalMaps: { [index: string]: number } = exchangeInnerMethod(process, "_signalMaps");
registerTempInnerMethod("signalMaps", signalMaps);

function kill(pid: number, signal?: number | string) {
    if (arguments.length === 0) {
        throw new RangeError(`Failed to execute 'kill': 1 argument required, but only ${arguments.length} present.`);
    }
    if (typeof pid !== 'number') {
        throw new TypeError("Invalid arguments, paramter pid must be number, but " + typeof pid + " is provided");
    }
    if (arguments.length == 1) {
        return innerKill(pid, signalMaps.SIGTERM);
    }
    const sigType = typeof signal;
    if (sigType === 'number') {
        return innerKill(pid, signal as number);
    }
    if (sigType === 'string') {
        if (!signalMaps.hasOwnProperty(signal as string)) {
            throw new TypeError("Invalid arguments, paramter signal is not invalid SIGNAL");
        }
        return innerKill(pid, signalMaps[signal as string]);
    }
    throw new TypeError("Invalid arguments, paramter signal must be number or string, but " + sigType + " is provided");
}

const innerGetEnviron: () => string[] = exchangeInnerMethod(process, "_innerGetEnviron");
const innerGetEnv: (name: string) => string | undefined = exchangeInnerMethod(process, "_innerGetEnv");
const innerSetEnv: (name: string, value: string) => number = exchangeInnerMethod(process, "_innerSetEnv");
const originEnviron: UniObject<string> = {};
const getCurrentEnv = () => {
    const result: UniObject<string> = {};
    (innerGetEnviron() as string[]).map((e) => {
        const [k, v] = e.split("=", 1);
        result[k] = v;
    });
    return result;
}

const innerUnsetEnv: (name: string) => number = exchangeInnerMethod(process, "_innerUnsetEnv");
const envProxy = new Proxy(originEnviron, {
    defineProperty(_target, _property, _attributes) {
        throw Error('env Object dose not support defineProperty.');
    },
    deleteProperty(_target, p) {
        if (typeof p === 'symbol') {
            throw new Error('env Object dose not support symbol.');
        }
        innerUnsetEnv(p);
        return true;
    },
    get(_target, p, _receiver) {
        if (typeof p === 'symbol') {
            throw new Error('env Object dose not support symbol.');
        }
        if (p === "valueOf") {
            return () => {
                return Object.valueOf.apply(getCurrentEnv());
            }
        }
        if (p === "toString") {
            return () => {
                return innerGetEnviron().join("\n");
            }
        }
        return innerGetEnv(p);
    },
    set(_target, p, newValue, _receiver) {
        if (typeof p == "symbol") {
            throw new Error('env Object dose not support symbol.');
        }
        if (typeof newValue !== 'string') {
            throw new Error('env value must be type of string.');
        }
        return innerSetEnv(p, newValue) === 0;
    },
    has(_target, p) {
        if (typeof p == "symbol") {
            throw new Error('env Object dose not support symbol.');
        }
        return innerGetEnv(p) !== void 0;
    },
    ownKeys(_target) {
        return Object.keys(getCurrentEnv());
    },
});

const innerGetTitle: () => string = exchangeInnerMethod(process, "_innerGetTitle")
const innerSetTitle: (title: string) => void = exchangeInnerMethod(process, "_innerSetTitle")
Reflect.defineProperty(process, "title", {
    get() {
        return innerGetTitle();
    },
    set(v: string) {
        innerSetTitle(v);
    },
});

const localCreateReadable: <T>(underlyingSource?: UnderlyingSource<T>) => Readable<T> = Reflect.get(globalThis, "createReadable");

{
    interface FdStdioReadableStrage extends StdioReadableStorage<number> {
        handler: ExternalObject;
    }

    const cachedData = new WeakMap<StdioReadable, StdioReadableStorage<any>>();
    const proc = process as { [index: string]: any };
    const innerReadFd: (fd: number,
        error?: (code: number, fd: number) => void,
        readable?: (fd: number) => void,
        writable?: (fd: number) => void,
        disconnect?: (fd: number) => void,
        prioritized?: (fd: number) => void,
    ) => ExternalObject = exchangeInnerMethod(process, "_innerReadFd");

    const readStdin = (storage: StdioReadableStorage<number>) => {
        const that = storage as FdStdioReadableStrage;
        if (that.data !== undefined) return;
        const handler = innerReadFd(that.fd, undefined,
            (fd: number) => {
                const bufferEndCode = '\n'.charCodeAt(0);
                const file = new File(fd, 'rb');
                let contents: number[] = [];
                let byte = new Uint8Array(file.read() as ArrayBuffer);
                while (byte[0] != bufferEndCode) {
                    for (const b of byte) {
                        contents.push(b);
                    }
                    byte = new Uint8Array(file.read() as ArrayBuffer);
                }
                for (const b of byte) {
                    contents.push(b);
                }
                const u8a = new Uint8Array(contents);
                that.controller.enqueue(u8a);
            });
        that.handler = handler;
        that.data = [];
    }
    class StdioReadable extends Readable<Uint8Array> {
        on(name: string, func: Function): void {
            if (name === 'data') {
                const data = cachedData.get(this)!;
                data.read(data);
            }
            super.on(name, func);
        }
    }
    const createStdioReadable = (fd: any, read: (storage: StdioReadableStorage<any>) => void): StdioReadable => {
        let controller: ReadableStreamDefaultController<Uint8Array>;
        const stdinReadable = localCreateReadable<Uint8Array>({
            start: (con: ReadableStreamDefaultController<any>) => {
                controller = con;
            }
        });
        Object.setPrototypeOf(stdinReadable, StdioReadable.prototype);
        cachedData.set(stdinReadable as StdioReadable, { fd: fd, controller: controller!, read: read });
        return stdinReadable;
    }
    registerTempInnerMethod("createStdioReadable", createStdioReadable);

    proc.stdin = createStdioReadable(0, readStdin);
    proc.stdout = new File(1, 'wb');
    proc.stderr = new File(2, 'wb');
}

const processEventTarget = new EventTarget();

interface UnhandledRejectionEvent extends Event {
    data: {
        reason: any,
        promise: Promise<any>,
    }
}
let unhandledRejectionRegistered = false;

{
    enum PromiseRejectionEventKind {
        PROMISE_REJECTION_EVENT_UNHANDLED,
        PROMISE_REJECTION_EVENT_HANDLED,
    }
    type rejectCallback = (event: PromiseRejectionEventKind, promise: Promise<any>, reason: any) => void;
    type checkCallback = () => void;
    const registerRejectionCallback: (callbackRef: rejectCallback, checkRef: checkCallback) => void = exchangeInnerMethod(globalThis, "_registerRejectionCallback");
    const rejectionPromises = new Map<Promise<any>, any>();
    registerRejectionCallback(
        (event: PromiseRejectionEventKind, promise: Promise<any>, reason: any) => {
            if (event === PromiseRejectionEventKind.PROMISE_REJECTION_EVENT_HANDLED) {
                rejectionPromises.delete(promise);
            } else {
                rejectionPromises.set(promise, reason);
            }
        },
        () => {
            let uncaught = 0;
            while (rejectionPromises.size > 0) {
                for (const [promise, reason] of rejectionPromises) {
                    if (unhandledRejectionRegistered) {
                        const e = new Event('unhandledRejection') as UnhandledRejectionEvent;
                        e.data = {
                            reason: reason,
                            promise: promise,
                        }
                        processEventTarget.dispatchEvent(e);
                    } else {
                        console.error("Uncaught exception (in promise)");
                        console.error(reason);
                    }
                    uncaught++;
                    rejectionPromises.delete(promise);
                }
            }
            if (!unhandledRejectionRegistered && uncaught > 0)
                process.exit(process.exitCode ?? 1);
        });
}

const processAddEventListener = (type: string, listener: Function) => {
    switch (type) {
        case 'unhandledRejection':
            if (typeof listener !== 'function') return;
            unhandledRejectionRegistered = true;
            processEventTarget.addEventListener(type, (e) => {
                const ev = e as UnhandledRejectionEvent;
                (listener as (reason: Error | any, promise: Promise<any>) => void)(ev.data.reason, ev.data.reason);
            });
            break;
    }
}

exportsToObject(process, [
    ['env', envProxy],
    ['kill', kill],
    ['on', processAddEventListener],
]);
